package org.raymond.iworks.study.performance.disk;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;

public class IOOptDemo {
    public static void main(String[] args) {

    }

    /**
     * java InputStream和OutputStream读取文件并通过socket发送，到底涉及几次拷贝
     * 总结
     * java BIO读取文件再通过socket发送,一共包含6次拷贝
     * --- read from disk ---
     * 1,调用系统函数read读取磁盘文件,用户态切换到内核态,底层调用DMA读取磁盘文件,把内容拷贝到内核的读写缓冲区(不消耗CPU)
     * 2,内核缓冲区数据拷贝到堆外内存,内核态转换外用户态,消耗CPU
     * 3,堆外内存拷贝到堆内内存,消耗CPU
     * --- write to socket ---
     * 4,堆内又拷贝到堆外内存,消耗CPU
     * 5,调用socket的send,用户态切换到内核态,堆外再拷贝到套接字缓冲区,消耗CPU
     * 6,send返回,内核态切换回用户态,同时DMA把数据从套接字缓冲区拷贝到协议引擎进行发送
     */
    public void bio() throws IOException {
        File file = new File("/path");
        FileInputStream in = new FileInputStream(file);
        // 假设一次可以读完
        byte[] buffer = new byte[1024];
        // 文件数据读取到bufffer数组
        // 1) 先通过malloc分配一个native堆内存buf,属于用户空间的堆外内存
        // 2) 通过GET_FD获取文件描述符,调用IO_Read将文件数据copy到buf中
        // 3) 调用SetByteArrayRegion,将buf(堆外内存)拷贝到buffer(java堆内存)
        // Q1: 调用IO_read时发生了什么
        // IO_Read其实就是一个c方法,会直接调用操作系统的read来读取文件数据到native堆内存buf中,这个过程涉及到两次拷贝:
        // a) DMA将文件读取到内核缓冲区(1次)
        // b) 内核将缓冲区读取到堆外内存buf(2次)
        // Q2: 为什么要先拷贝到堆外空间,再拷贝到堆内
        // 一个重要的前提是,java和c++都无法直接操作内核缓冲区的数据,只有用户空间的操作权限,在这个前提下,理想情况有两种:
        // a) 直接将内核缓冲区的数据拷贝到堆内存
        // 为啥不行?因为jvm的gc一直在不断的整理内存,内存地址可能会发生变化,如果native希望将数据拷贝到堆内存,那么每一次拷贝都必须将jvm暂停来
        // 保证gc不出错.而IO_Read方法中拿到的java传过来的byte数组的引用,在将buf拷贝给这个引用时,即使内存地址变了,jvm也能够知道它真正的地址
        // 在哪里
        // 我的理解:即如果直接将内核缓存区的数据写入堆内存,这就相当于只是开辟了一个内存空间并存入了数据,但这块内存并没有被jvm持有的引用
        // b) 内核缓冲区拷贝数据到堆外内存
        // 由jvm直接操作,nio的directBuffer就是这样做的.
        // 所以,将文件读取到jvm堆内存,一共涉及到三次拷贝:
        // 1) DMA拷贝文件数据到内核缓冲区
        // 2) CPU拷贝内核数据到堆外内存(native堆)
        // 3) CPU拷贝堆外内存到java堆内
        in.read(buffer);
        // 开启socket
        Socket socket = new Socket("localhost", 9099);
        // 获取SocketOutputStream
        OutputStream os = socket.getOutputStream();
        // 数据写出去
        // 1.获取文件描述符acquireFD
        // 2.调用native的socketWrite0
        // 3.开辟一个native的缓存buf
        // 4.把java传过来的数据拷贝给native buf
        // 5.调用NET_Send发送数据
        // -> 调用系统函数send
        // ->-> 1) CPU将堆外内存拷贝到内核的套接字缓冲区
        // ->-> 2) DMA将套接字缓冲区数据拷贝到协议引擎进行传输
        os.write(buffer);
    }

    /**
     * java NIO读取文件并通过socket发送，最少拷贝了几次？堆外内存和所谓的零拷贝到底是什么关系
     * 内核的零拷贝
     * 内核的零拷贝,指的是不需要消耗CPU资源,完全交给DMA来处理,内核空间的数据没有多余的拷贝.主要经历了这么几个发展历程:
     * 一,传统的read + send
     * 1、先调用操作系统的read函数,由DMA将文件拷贝到内核,然后CPU把内核数据拷贝到用户缓冲区(堆外内存)
     * 2、调用操作系统的send函数,由CPU把用户缓冲区的数据拷贝到socket缓冲区,最后DMA把socket缓冲区数据拷贝到网卡进行发送.
     * 这个过程中内核数据拷贝到用户空间,用户空间又拷贝回内存,有两次多余的拷贝.
     * 二、sendfile改进版本,真正的零拷贝
     * 内核为2.4或者以上版本的linux系统上,改进后的处理过程如下：
     * 1、DMA 将磁盘数据拷贝到内核缓冲区,向socket缓冲区中追加当前要发送的数据在内核缓冲区中的位置和偏移量
     * 2、DMA gather copy 根据 socket缓冲区中的位置和偏移量,直接将内核缓冲区中的数据拷贝到网卡上.
     * 经过上述过程,数据只经过了 2 次 copy 就从磁盘传送出去了.并且没有CPU的参与.
     *
     * java的零拷贝
     * 一、利用directBuffer
     * 在 bio 中，基于BIO读取文件发送消息,一共涉及六次拷贝,其中堆外和堆内内存的拷贝是多余的,我们可以利用directBuffer来减少这两次拷贝
     *    fileChannel和socketChannel配合directBuffer,都是配合系统函数write和read,对文件描述符直接操作堆外内存.因此相比较于BIO可以
     * 省去两次拷贝
     * 二、channel.transferTo
     * java中的零拷贝就是依赖操作系统的sendfile函数来实现的,提供了channel.transferTo方法,允许将一个channel的数据直接发送到另一个channel
     */
    public void directBuf() throws IOException {
        //打开文件通道
        FileChannel fileChannel = FileChannel.open(Paths.get("/test.txt"));
        //申请堆外内存
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        //读取到堆外内存
        // 调用IOUtil,根据文件描述符fd读取数据到堆外内存 byteBuffer中
        fileChannel.read(byteBuffer);
        //打开socket通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9099));
        //堆外内存写入socket通道
        socketChannel.write(byteBuffer);
    }

    /**
     * 总结
     * 总结
     * NIO读取文件并通过socket发送,最少拷贝几次?
     * 直接调用channel.transferTo,同时linux内核版本大于等于2.4,则可以将拷贝次数降低到2次,并且CPU不参与拷贝.
     * 堆外内存和所谓的零拷贝到底是什么关系
     * 笔者理解网上说的零拷贝,可以理解为内核层面的零拷贝和java层面的零拷贝,所谓的0并不是一次拷贝都没有,而是在不同的场景下尽可能减少拷贝次数.
     */
    public void transferTo() throws IOException {
        //打开socketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9099));
        //
        FileChannel fileChannel = FileChannel.open(Paths.get("/test.txt"));
        fileChannel.transferTo(0, fileChannel.size(), socketChannel);
    }
}
